No description has been provided for this image

Introducción a la Visión Computacional

Tarea 1

Magíster en Data Science¶

Cristhian Solís Muñoz¶


Fecha de Entrega: Martes 22, Abril 2025.

Proyecto Clasificación de patologías en peces de agua dulce a través de Visión Computacional 🐟

Banner Acuicultura

Resumen¶

Se desarrolla los primeros pasos para la generación de un proyecto ligado con visión computacional, partiendo desde la selección de una base de datos de imágenes abierta, aplicando preprocesamiento básico de las imágenes del dataset y finalizando con la definición del plan de trabajo para las siguientes etapas.

Librerias¶

Se exponen las librerias utilizadas durante el informe

In [1]:
# Importación de librerias
import os
from PIL import Image
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2

Aplicación Propuesta¶

En esta etapa inicial, se propone el desarrollo de un clasificador de imágenes que permita identificar si un pez está sano o si presenta signos visibles de enfermedad. No obstante, se contemplan otras posibles líneas de investigación y desarrollo que podrán ser abordadas en entregas posteriores.

Esta propuesta inicial se alinea con el objetivo de explorar el potencial de la visión computacional como herramienta de apoyo en la detección temprana de enfermedades, contribuyendo así a la mejora del bienestar animal y la eficiencia productiva en la industria acuícola.

  • Objetivo: Clasificar la condición sanitaria de un pez.
  • Justificación: Esta tarea es relevante en el contexto de la acuicultura, donde la detección temprana de enfermedades puede prevenir pérdidas económicas significativas y vela por el bienestar animal.

Descripción y Exploración del Dataset¶

En los proximos bloques, se describe y explora el dataset obtenido medieante Kaggle. Denominado Freshwater Fish Disease Aquaculture in south asia.

Descripción del dataset¶

El conjunto de datos está compuesto por imágenes recolectadas desde diversas fuentes, incluyendo el departamento de investigación de una universidad, centros acuícolas y portales en línea. La recopilación fue realizada con el apoyo de expertos en sanidad de peces, lo que respalda la validez y precisión de las etiquetas asignadas.

Las imágenes corresponden a diferentes patologías observadas en peces cultivados en sistemas de acuicultura, específicamente durante su etapa en agua dulce.

Carga del dataset¶

Se carga la carpeta base, se crea un directorio para almacenar las imagenes resultantes del pre procesamiento del dataset. Se identifican las clases y genera un dataframe con metadatos.

In [2]:
# Ruta donde están las imágenes
base_path = r'C:\Users\csolis\OneDrive - Nutreco Nederland B.V\Desktop\Freshwater-Fish-Disease-Aquaculture\Freshwater-Fish-Disease-Aquaculture\Data\Train'
processed_base_path = r'C:\Users\csolis\OneDrive - Nutreco Nederland B.V\Desktop\Freshwater-Fish-Disease-Aquaculture\Freshwater-Fish-Disease-Aquaculture\Data\Train_Processed'

# Crear directorio para imágenes procesadas si no existe
if not os.path.exists(processed_base_path):
    os.makedirs(processed_base_path)

# Listar clases (subcarpetas)
clases = os.listdir(base_path)
clases_processed = os.listdir(processed_base_path)
In [3]:
# Generación DF metadatos
# Lista para almacenar metadata
metadata = []

# Recorrer las carpetas/clases
for clase in clases:
    clase_path = os.path.join(base_path, clase)
    for nombre_img in os.listdir(clase_path):
        ruta_img = os.path.join(clase_path, nombre_img)
        try:
            with Image.open(ruta_img) as img:
                formato = img.format
                ancho, alto = img.size
                modo_color = img.mode  # RGB, L, etc.
                shape = img.size + (3,) if modo_color == 'RGB' else img.size + (1,)  # Agregar canal de color
                metadata.append({
                    'clase': clase,
                    'archivo': nombre_img,
                    'formato': formato,
                    'ancho': ancho,
                    'alto': alto,
                    'modo_color': modo_color,
                    'shape': shape
                })
        except Exception as e:
            print(f"Error al leer {ruta_img}: {e}")

# Crear DataFrame con la metadata
df_metadata = pd.DataFrame(metadata)

Exploración del Dataset¶

Se realiza una exploración inicial del dataset con el objetivo de comprender su estructura y características principales. Para ello, se abordan las siguientes preguntas clave:

  • ¿Cuál es el tamaño del dataset?
  • ¿Cuáles son las clases disponibles y cuántas hay?
  • ¿Cuántas imágenes hay por clase?
  • ¿Qué tipos de archivos contiene el dataset?
  • ¿Cuáles son los tamaños de los archivos existentes en el dataset?
  • ¿Cuáles son las dimensiones (shape) de las imágenes?
  • ¿Qué tipos de modo de color existen?
In [4]:
# ¿Cuál es el tamaño del dataset?
total_imagenes = len(df_metadata)
print(f"El Dataset contiene {total_imagenes} imagenes")
El Dataset contiene 1750 imagenes
In [5]:
# ¿Cuáles son las clases disponibles y cuántas hay? 
clases_unicas = sorted(df_metadata['clase'].unique())
num_clases = len(clases_unicas)

print(f"Existen {num_clases} clases")
print("Las cuáles son:")
for i, clase in enumerate(clases_unicas, start=1):
    print(f"  {i}. {clase}")
Existen 7 clases
Las cuáles son:
  1. Bacterial Red disease
  2. Bacterial diseases - Aeromoniasis
  3. Bacterial gill disease
  4. Fungal diseases Saprolegniasis
  5. Healthy Fish
  6. Parasitic diseases
  7. Viral diseases White tail disease

Se describen las clases identificadas y una breve descripción de la clase

Clase Descripción
Bacterial Red disease Enfermedad bacteriana con manchas rojas
Bacterial diseases - Aeromoniasis Infección bacteriana causada por Aeromonas
Bacterial gill disease Enfermedad bacteriana que afecta las branquias
Fungal diseases Saprolegniasis Infección fúngica que afecta la piel del pez
Healthy Fish Peces sanos sin signos de enfermedad
Parasitic diseases Enfermedades causadas por parásitos
Viral diseases White tail disease Enfermedad viral que afecta la cola del pez
In [6]:
# ¿Cuántas imágenes hay por clase?  
conteo_por_clase = df_metadata['clase'].value_counts().reset_index(name='cantidad').rename(columns={'index': 'clase'})
# Crear gráfico de barras
ax = conteo_por_clase.plot(kind='barh', 
                           x='clase', 
                           y='cantidad',
                           ylabel='Clases',
                           color='skyblue',
                           legend=False,
                           title='Cantidad de imágenes por clase')

# Agregar los totales sobre cada barra
for p in ax.patches:
    ax.annotate(f'{p.get_width()}', 
                (p.get_width(), p.get_y() + p.get_height() / 2.),
                xytext=(10, 0), textcoords='offset points', 
                ha='left', va='center', fontsize=10, color='black')
    
# Ajustar el gráfico para mejor presentación
plt.tight_layout()
ax.xaxis.set_visible(False)
for spine in ax.spines.values():
    spine.set_visible(False)
plt.show()
No description has been provided for this image

Se observa una distribución uniforme, con 250 archivos por clase en el dataset, lo que representa un escenario ideal para el modelado de datos dado que las clases se encuentran balanceadas

In [7]:
# ¿Qué tipos de archivos contiene el dataset?
print("\nTipos de formato de imagen:")
print(df_metadata['formato'].value_counts())
print("\n-----")
Tipos de formato de imagen:
formato
JPEG    1718
PNG       29
WEBP       3
Name: count, dtype: int64

-----

El dataset incluye imágenes en tres formatos distintos. Para los fines de este proyecto, se sugiere el uso exclusivo de imágenes fotográficas de carácter productivo.

En ese sentido, se recomienda trabajar únicamente con archivos en formato JPEG, ya que representan de mejor forma condiciones reales de captura y ofrecen mayor consistencia para procesos de entrenamiento en modelos de visión computacional.

In [8]:
# ¿Cuáles son los tamaños de los archivos existentes en el dataset?
print("\nTamaños más frecuentes:")
print(df_metadata.groupby(['ancho', 'alto']).size().sort_values(ascending=False).head(5))
Tamaños más frecuentes:
ancho  alto
128    128     1291
224    224      449
184    136        1
116    212        1
224    62         1
dtype: int64
In [9]:
# ¿Cuáles son las dimensiones (shape) de las imágenes?
print("\nDimensiones (shape) de las imágenes:")
print(df_metadata['shape'].value_counts())
Dimensiones (shape) de las imágenes:
shape
(128, 128, 3)    1291
(224, 224, 3)     449
(224, 80, 3)        1
(224, 109, 3)       1
(224, 62, 3)        1
(184, 136, 3)       1
(224, 110, 3)       1
(116, 212, 3)       1
(224, 71, 3)        1
(224, 69, 3)        1
(224, 73, 3)        1
(224, 97, 3)        1
Name: count, dtype: int64
In [10]:
# ¿Qué tipos de modo de color existen?
print("\nModos de color de las imágenes:")
print(df_metadata['modo_color'].value_counts())
print("\n-----")
Modos de color de las imágenes:
modo_color
RGB    1750
Name: count, dtype: int64

-----

Resumiendo las características de las imágenes presentes en el dataset, el conjunto de datos está compuesto por imágenes en modo de color RGB, con tres canales, lo que resulta adecuado para representar información visual en tareas de VC.

En cuanto a las dimensiones, predominan dos tamaños principales: 128x128 y 224x224 píxeles. Sin embargo, también se identifican algunas imágenes con resoluciones atípicas. Esta variabilidad refuerza la importancia de estandarizar las dimensiones durante el preprocesamiento, con el fin de preparar el set de datos para modelos de aprendizaje profundo.

In [14]:
# Visualización de las imagenes del dataset
for img_name in random.sample(df_metadata['archivo'].tolist(), 3):
    img_path = os.path.join(base_path, df_metadata.loc[df_metadata['archivo'] == img_name, 'clase'].values[0], img_name)
    
    try:
        img = Image.open(img_path)
        plt.imshow(img)
        plt.title(f"Clase: {df_metadata.loc[df_metadata['archivo'] == img_name, 'clase'].values[0]}")
        plt.axis('off')
        plt.show()
    except FileNotFoundError:
        print(f"Imagen no encontrada: {img_path}")
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Se finaliza esta etapa con la visualización de tres imágenes aleatorias del dataset, con el objetivo de observar ejemplos del conjunto de datos en su estado original antes del preprocesamiento.

Preprocesamiento¶

Se generan distintas acciones para preprocesar el set de imagenes, con foco en la aplicacion propuesta previamente.

Se realizan acciones, cómo, reescalar las dimensiones buscando la estandarización para habilitar su empaquetamiento en batch uniformes. Seleccionar estrateficada un único formato JPEG, con el objetivo de eliminar imagenes de origen digital. Finalmente, se almacenan las imagenes resultantes en un nuevo directorio para la manteción del dataset original y se expone un set de ejemplo.

Redimensionamiento¶

En esta etapa, las imágenes se redimensionan a un tamaño estándar (512x512 píxeles), lo que permite asegurar que todas las imágenes sean compatibles para su procesamiento en batch y se procesan solo imagenes en formato JPEG.

Este proceso es crucial para preparar el dataset para entrenamiento de modelos, garantizando que las imágenes tengan un tamaño uniforme.

In [15]:
# Función para redimensionar la imagen
def redimenzionar_y_formato(image_path):
    img = Image.open(image_path)
    img_resized = img.convert('RGB').resize((512, 512)) 
    return np.array(img_resized)

# Recorrer cada clase y procesar las imágenes
for clase in clases:
    clase_path = os.path.join(base_path, clase)
    processed_clase_path = os.path.join(processed_base_path, clase)

    # Crear directorio para cada clase procesada si no existe
    if not os.path.exists(processed_clase_path):
        os.makedirs(processed_clase_path)

    # Procesar imágenes dentro de cada clase
    for nombre_img in os.listdir(clase_path):
        ruta_img = os.path.join(clase_path, nombre_img)
        
        # Solo procesar imágenes JPEG para considerar origen "productivo real" y no permitir formatos digital
        if ruta_img.lower().endswith('.jpeg') or ruta_img.lower().endswith('.jpg'):
            try:
                # Redimensionar y convertir la imagen
                img_resized = redimenzionar_y_formato(ruta_img)
                
                # Guardar imagen procesada
                processed_img_path = os.path.join(processed_clase_path, nombre_img)
                Image.fromarray(img_resized).save(processed_img_path)
                
            except Exception as e:
                print(f"Error al procesar {ruta_img}: {e}")

Aplicación de Filtro Gaussiano¶

Una vez redimensionadas las imágenes, se aplica un filtro Gaussiano con el objetivo de suavizar el ruido y mejorar la calidad visual de las imágenes. Este tipo de preprocesamiento ayuda a reducir pequeñas variaciones no representativas en los datos.

El filtro se aplica a todas las imágenes del conjunto, sobrescribiendo cada archivo con su versión suavizada.

In [16]:
# Función para aplicar filtro gaussiano
def filtro_gaussiano(image):
    return cv2.GaussianBlur(image, (5, 5), 0)

# Recorrer las imágenes redimensionadas y aplicar el filtro gaussiano
for clase in clases:
    clase_path = os.path.join(processed_base_path, clase)
    
    for nombre_img in os.listdir(clase_path):
        ruta_img = os.path.join(clase_path, nombre_img)
        try:
            # Cargar la imagen procesada
            img = cv2.imread(ruta_img)
            
            # Aplicar el filtro gaussiano
            img_filtered = filtro_gaussiano(img)
            
            # Guardar la imagen procesada con el filtro
            cv2.imwrite(ruta_img, img_filtered)
        
        except Exception as e:
            print(f"Error al aplicar filtro gaussiano a {ruta_img}: {e}")

Validaciones¶

Se realizan diversas validaciones para asegurar la calidad del preprocesamiento aplicado al dataset, incluyendo:

  • Verificación de la presencia de todas las clases definidas.
  • Confirmación de que todas las imágenes comparten el mismo formato (JPEG).
  • Comprobación de la estandarización en las dimensiones de las imágenes.
  • Detección de archivos corruptos o no legibles.
  • Cálculo del tamaño total del dataset tras el preprocesamiento.

Finalmente, se presenta una comparación visual entre el conjunto original y el conjunto procesado, destacando los principales cambios.

In [17]:
# Verificar si todas las clases de Train están presentes en Train_Processed
clases_train = os.listdir(base_path)
clases_processed = os.listdir(processed_base_path)

# Comprobar si todas las clases de Train están en Train_Processed
missing_classes = set(clases_train) - set(clases_processed)
if missing_classes:
    print(f"Faltan las siguientes clases en Train_Processed: {missing_classes}")
else:
    print("Todas las clases están presentes en Train_Processed.")
Todas las clases están presentes en Train_Processed.
In [18]:
# Verificar el formato de las imágenes procesadas
for clase in clases_processed:
    clase_path = os.path.join(processed_base_path, clase)
    for img_name in os.listdir(clase_path):
        img_path = os.path.join(clase_path, img_name)
        with Image.open(img_path) as img:
            if img.format != 'JPEG':
                print(f"Imagen no JPEG encontrada: {img_path}")
In [19]:
# Verificar dimensiones de las imágenes procesadas
for clase in clases_processed:
    clase_path = os.path.join(processed_base_path, clase)
    for img_name in os.listdir(clase_path):
        img_path = os.path.join(clase_path, img_name)
        with Image.open(img_path) as img:
            if img.size != (512, 512):
                print(f"Dimensiones incorrectas en la imagen: {img_path}")
In [20]:
# Verificar imágenes corruptas
for clase in clases_processed:
    clase_path = os.path.join(processed_base_path, clase)
    for img_name in os.listdir(clase_path):
        img_path = os.path.join(clase_path, img_name)
        try:
            with Image.open(img_path) as img:
                img.verify()  # Verifica que la imagen no esté corrupta
        except (IOError, SyntaxError) as e:
            print(f"Imagen corrupta encontrada: {img_path}")
In [21]:
# Contar imágenes por clase
conteo_por_clase = {}
for clase in clases_processed:
    clase_path = os.path.join(processed_base_path, clase)
    conteo_por_clase[clase] = len(os.listdir(clase_path))

# Convertir el conteo a un DataFrame
df_conteo = pd.DataFrame(list(conteo_por_clase.items()), columns=['clase', 'cantidad'])
df_conteo = df_conteo.sort_values(by='cantidad', ascending=True)

# Gráfico horizontal con totales
ax = df_conteo.plot(kind='barh', 
                    x='clase', 
                    y='cantidad',
                    ylabel='Clases',
                    legend=False,
                    title='Cantidad de imágenes por clase',
                    color='skyblue')

# Agregar los totales sobre cada barra
for p in ax.patches:
    ax.annotate(f'{p.get_width()}', 
                (p.get_width(), p.get_y() + p.get_height() / 2.),
                xytext=(10, 0), textcoords='offset points', 
                ha='left', va='center', fontsize=10, color='black')

# Ajustar presentación
plt.tight_layout()
ax.xaxis.set_visible(False)
for spine in ax.spines.values():
    spine.set_visible(False)

plt.show()
No description has been provided for this image

Al procesar exclusivamente imágenes en formato JPEG, se observa una reducción en la cantidad total de archivos por clase. Este ajuste afecta por igual a todas las categorías, manteniendo así la proporcionalidad original del dataset y asegurando un equilibrio adecuado entre clases para entrenamiento de modelos.

In [22]:
# Contar el total de imágenes en todas las clases
total_imagenes = 0
for clase in clases_processed:
    clase_path = os.path.join(processed_base_path, clase)
    total_imagenes += len(os.listdir(clase_path))

# Mostrar el total de archivos
print(f"Total de imágenes procesadas: {total_imagenes}")
Total de imágenes procesadas: 1718

Como se observó durante la exploración del dataset, este contiene un total de 1.718 imágenes en formato JPEG, las cuales se encuentran íntegramente en el directorio Train_Processed. Esto valida que los pasos de procesamiento fueron aplicados correctamente a la totalidad de los archivos definidos como objetivo de trabajo.

In [23]:
# Comparacion de imagenes procesadas
for clase in clases:
    # Elegir una imagen de la clase
    img_name = os.listdir(os.path.join(base_path, clase))[0]  # Tomamos la primera imagen de la clase

    # Rutas de la imagen original y procesada
    original_img_path = os.path.join(base_path, clase, img_name)
    processed_img_path = os.path.join(processed_base_path, clase, img_name)

    # Cargar imágenes original y procesada
    original_img = Image.open(original_img_path)
    processed_img = Image.open(processed_img_path)

    # Mostrar comparativa de imágenes
    plt.figure(figsize=(12, 6))
    plt.subplot(1, 2, 1)
    plt.imshow(original_img)
    plt.title(f"Original: {clase}")
    plt.axis('off')
    plt.subplot(1, 2, 2)
    plt.imshow(processed_img)
    plt.title(f"Procesada: {clase}")
    plt.axis('off')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Finalmente, se expone el antes y después de la primera imagen de cada clase, permitiendo visualizar de forma clara los efectos del preprocesamiento aplicado.

Plan Futuro¶

En esta etapa inicial, se propone el desarrollo de un clasificador de imágenes que permita identificar el estado de salud de un pez a partir de signos visibles. A diferencia de un modelo binario (sano vs. enfermo), este proyecto propone un enfoque multiclase, considerando 7 categorías que representan diversas condiciones o enfermedades detectables visualmente.

El objetivo es construir una herramienta de clasificación visual que sirva como apoyo temprano en la detección de problemas sanitarios, contribuyendo a una reducción en los tiempos de diagnóstico y a una toma de decisiones más eficiente en el ámbito acuícola.

Se propone el desarrollo de un modelo de deep learning, específicamente utilizando redes neuronales convolucionales (CNN), explorando distintas arquitecturas y configuraciones para identificar la opción óptima según métricas relevantes como accuracy, recall y F1-score, con un foco particular en reducir los falsos negativos (es decir, peces enfermos clasificados como sanos).

Si el prototipo alcanza resultados satisfactorios, se podría proponer adaptación a la industria chilena. Con un proyecto end-to-end, desde la generación del set con el contexto chileno, hasta el despliegue del modelo. Esto permitiría integrar el modelo como parte del monitoreo rutinario automatizado, abriendo incluso la posibilidad de desarrollar un producto monetizable basado en datos.